文件预览下载

吴龙淼 写于 2022-03-25

文档预览

目前实现大致有三种解决方案:

1.使用微软提供的接口。这个方案虽然省事,而且效果是最好的,但是文件 url 必须是公网可访问的,对于局域网部署或者文件保密的情况并不适用(谷歌文档在线预览)

src=”‘https://docs.google.com/viewer?url=url src=’https://view.officeapps.live.com/op/view.aspx?src=url 金山文档在线预览,私有化部署 使用 Java 后端统一转换,在前端预览。该方案兼容性较好,效果仅次于微软提供接口,但是对于服务器的压力比较大,高并发,吞吐量场景不适用 纯前端将文件转换成 html,效果不太理想,文件格式未必能保留,老文件格式 doc 不支持,不消耗服务器性能

前端方案

文档格式 开源方案
word(docx) mammoth,docx-preview
excel(xlsx) exceljs,Luckysheet,handsontable
powerpoint(pptx) pptxjs
pdf(pdf) pdfjs

测试路径

服务端预览

前端预览

前端文件下载

form 表单

兼容性好,不知道下载进度,不能直接下载浏览器直接预览的文件txt,png

/**
 * 下载文件
 * @param {String} fileName - 文件名
 */
function downloadFile (downloadUrl, fileName) {
    // 创建表单
    const formObj = document.createElement('form');
    formObj.action = downloadUrl;
    formObj.method = 'get';
    formObj.style.display = 'none';
    // 创建input,主要是起传参作用
    const formItem = document.createElement('input');
    formItem.value = fileName; // 传参的值
    formItem.name = 'fileName'; // 传参的字段名
    // 插入到网页中
    formObj.appendChild(formItem);
    document.body.appendChild(formObj);
    formObj.submit(); // 发送请求
    document.body.removeChild(formObj); // 发送完清除掉
}

open 或 location.href

不能直接下载浏览器直接预览的文件txt,png,不能鉴权,不能查看下载进度,url长度限制

window.open('downloadFile.zip');

location.href = 'downloadFile.zip';

a 标签的 download

不能鉴权,h5新特性,不能下载跨域浏览器可浏览的文件,可下载浏览器直接与预览的文件

<a href="example.jpg" download='test'>点击下载</a>

利用 Blob 对象

兼容性

/**
 * 下载文件
 * @param {String} path - 下载地址/下载请求地址。
 * @param {String} name - 下载文件的名字/重命名(考虑到兼容性问题,最好加上后缀名)
 */
downloadFile (path, name) {
    const xhr = new XMLHttpRequest();
    xhr.open('get', path);
    xhr.responseType = 'blob';
    xhr.send();
    xhr.onload = function () {
        if (this.status === 200 || this.status === 304) {
            // 如果是IE10及以上,不支持download属性,采用msSaveOrOpenBlob方法,但是IE10以下也不支持msSaveOrOpenBlob
            if ('msSaveOrOpenBlob' in navigator) {
                navigator.msSaveOrOpenBlob(this.response, name);
                return;
            }
            // const blob = new Blob([this.response], { type: xhr.getResponseHeader('Content-Type') });
            // const url = URL.createObjectURL(blob);
            const url = URL.createObjectURL(this.response);
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = url;
            a.download = name;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }
    };
}

利用 base64

兼容性

/**
 * 下载文件
 * @param {String} path - 下载地址/下载请求地址。
 * @param {String} name - 下载文件的名字(考虑到兼容性问题,最好加上后缀名)
 */
downloadFile (path, name) {
    const xhr = new XMLHttpRequest();
    xhr.open('get', path);
    xhr.responseType = 'blob';
    xhr.send();
    xhr.onload = function () {
        if (this.status === 200 || this.status === 304) {
            const fileReader = new FileReader();
            fileReader.readAsDataURL(this.response);
            fileReader.onload = function () {
                const a = document.createElement('a');
                a.style.display = 'none';
                a.href = this.result;
                a.download = name;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
            };
        }
    };
}

文件上传

大文件上传

 // 文件二进制流,基础单位字节Byte
 const [firstFile] = e.target.files;
 // 初始化切片大小10M
 const SIZE = 10 * 1024 * 1024
 // 生成文件切片    (客户端文件上传事件触发时执行)
 createFileChunk(file, size = SIZE) {
  const result = [];
   let cur = 0;
   while (cur < file.size) {
     result.push({ fileItem: file.slice(cur, cur + size) });
     cur += size;
   }
   return result
 }
  // 上传按钮触发时执行
 async handleUpload() {
   if (!firstFile) return;
   const result = createFileChunk(firstFile);
   sliceData = result.map(({ fileItem },index) => (
    {
     chunk: fileItem,
     hash: firstFile.name + "-" + index // 文件名 + 数组下标
    }
   ));
   await uploadChunks();
 }
// 文件上传
async uploadChunks() {
   const requestList = sliceData
     .map(({ chunk,hash }) => {
       const formData = new FormData();
       formData.append("chunk", chunk);
       formData.append("hash", hash);
       formData.append("filename", firstFile.name);
       return fetch("http://localhost:3000",{
                                        method:'POST',
                                        body: formData
                                    })
     })
   // 并发切片
   await Promise.all(requestList);
   // 前端发送合并切片请求
   await fetch("http://localhost:3000/merge",{
     method:'POST',
     headers: {
       "content-type": "application/json"
     },
     body: JSON.stringify({
       filename: firstFile.name
     })
   })
}

断点续传

断点续传:前端/服务端需要记住已上传的切片

方案1.前端使用缓存 localStorage 记录已上传的切片 hash 方案2.服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片 在前面使用文件名+切片下标作为hash值,一旦文件名改变就会失效,应该根据内容生成hash,只要内容不变就不会失效 spark-md5:根据文件内容计算出文件的 hash 值,另开一个线程防止堵塞worker 线程计算 hash

hash生成优化
// 根据文件内容生成 hash
// 要计算所有文件切片的 hash 值,否则hash有大概率相同

self.importScripts("/spark-md5.min.js"); // 导入脚本

self.onmessage = e => {
  const { fileChunkList } = e.data;
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;
  const loadNext = index => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(fileChunkList[index].file);
    reader.onload = e => {
      count++;
      spark.append(e.target.result);
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end()
        });
        self.close();
      } else {
        percentage += 100 / fileChunkList.length;
        self.postMessage({
          percentage
        });
        // 递归计算下一个切片
        loadNext(count);
      }
    };
  };
  loadNext(0);
};


// 主线程与worker通讯

	   // 生成文件 hash(web-worker)
    calculateHash(fileChunkList) {
      return new Promise(resolve => {
       // 添加 worker 属性
        this.container.worker = new Worker("/hash.js");
        this.container.worker.postMessage({ fileChunkList });
        this.container.worker.onmessage = e => {
          const { percentage, hash } = e.data;
          this.hashPercentage = percentage;
          if (hash) {
            resolve(hash);
          }
        };
      });
    },
    async handleUpload() {
      if (!this.container.file) return;
      const fileChunkList = this.createFileChunk(this.container.file);
    this.container.hash = await this.calculateHash(fileChunkList);
      this.data = fileChunkList.map(({ file },index) => ({
      fileHash: this.container.hash,
        chunk: file,
        hash: this.container.file.name + "-" + index, // 文件名 + 数组下标
        percentage:0
      }));
      await this.uploadChunks();
    }

文件暂停
中断请求,并清空正在上传的切片

1.axios

const CancelToken = axios.CancelToken.source()
axios.get(url, {
  cancelToken: CancelToken.token
}).catch((thrown)=> {
  if (axios.isCancel(thrown)) {
    console.log('请求已取消', thrown.message);
  }
});
axios.post(url, {
    name:"test",
    value:'3242'
},{
  cancelToken: CancelToken.token
}).catch((thrown)=> {
  if (axios.isCancel(thrown)) {
    console.log('请求已取消', thrown.message);
  }
});
// or
axios({
  url:url,
  params:{
    name: 'new name'
  },
  cancelToken: CancelToken.token
})
//取消请求
CancelToken.cancel('取消请求!');

2.fetch

let abortController = new AbortController();
fetch(url, { signal: abortController.signal })
abortController.abort()

3.ajax

let ajax = new XMLHttpRequest()
ajax.open('GET/POST',url,false)
ajax.send()
ajax.onreadystatechange = ()=>{
    if(ajax.status ===200 && ajax.readyState = 4){
        console.log(ajax.response)
    }
}
ajax.abort()
文件秒传,续传

文件秒传依赖断点续传生成的 hash,把生成的 hash 发送给服务端进行验证,服务端一旦找到 hash 相同的文件,直接返回上传成功的信息 续传需要向服务器验证已上传的切片,前端过滤

前端每次上传前发送一个验证的请求,返回三种结果
    服务端已存在该文件,不需要再次上传
    服务端不存在该文件,完全上传资源
    服务端存在部分文件,并把已上传的文件切片返回给前端,前端过滤已经上传切片